package com.xiaoyu.im.client.proxy.nim;

import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.netease.nimlib.sdk.NIMClient;
import com.netease.nimlib.sdk.Observer;
import com.netease.nimlib.sdk.RequestCallback;
import com.netease.nimlib.sdk.RequestCallbackWrapper;
import com.netease.nimlib.sdk.ResponseCode;
import com.netease.nimlib.sdk.StatusCode;
import com.netease.nimlib.sdk.auth.AuthService;
import com.netease.nimlib.sdk.auth.AuthServiceObserver;
import com.netease.nimlib.sdk.auth.LoginInfo;
import com.netease.nimlib.sdk.auth.constant.LoginSyncStatus;
import com.netease.nimlib.sdk.event.EventSubscribeService;
import com.netease.nimlib.sdk.event.EventSubscribeServiceObserver;
import com.netease.nimlib.sdk.event.model.Event;
import com.netease.nimlib.sdk.event.model.EventSubscribeRequest;
import com.netease.nimlib.sdk.event.model.NimEventType;
import com.netease.nimlib.sdk.event.model.NimOnlineStateEvent;
import com.netease.nimlib.sdk.msg.MessageBuilder;
import com.netease.nimlib.sdk.msg.MsgService;
import com.netease.nimlib.sdk.msg.MsgServiceObserve;
import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.nimlib.sdk.msg.model.BroadcastMessage;
import com.netease.nimlib.sdk.msg.model.CustomNotification;
import com.netease.nimlib.sdk.msg.model.IMMessage;
import com.netease.nimlib.sdk.msg.model.LocalAntiSpamResult;
import com.netease.nimlib.sdk.msg.model.MessageReceipt;
import com.netease.nimlib.sdk.msg.model.QueryDirectionEnum;
import com.netease.nimlib.sdk.msg.model.RecentContact;
import com.netease.nimlib.sdk.msg.model.RevokeMsgNotification;
import com.xiaoyu.base.AppConfig;
import com.xiaoyu.base.utils.ListUtil;
import com.xiaoyu.im.IMCallback;
import com.xiaoyu.im.client.proxy.AbstractProxy;
import com.xiaoyu.im.datamodel.AppMessage;
import com.xiaoyu.im.datamodel.AppMessageReceipt;
import com.xiaoyu.im.datamodel.IMOnlineStatus;
import com.xiaoyu.im.datamodel.IMVendorType;
import com.xiaoyu.im.datamodel.auth.IMTokenInfo;
import com.xiaoyu.im.datamodel.bridge.MessageBridge;
import com.xiaoyu.im.datamodel.bridge.NimMessageBridge;
import com.xiaoyu.im.datamodel.common.IMConstant;
import com.xiaoyu.im.datamodel.exception.IMException;
import com.xiaoyu.im.datamodel.messagesync.MessageFetchResult;
import com.xiaoyu.im.datamodel.vendors.NimConversationUnique;
import com.xiaoyu.im.event.OnlineUserAccountEvent;
import com.xiaoyu.im.log.IMLogClient;
import com.xiaoyu.im.provider.ChatIdMapper;
import com.xiaoyu.im.provider.ConversationManager;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import in.srain.cube.concurrent.AppThreads;
import in.srain.cube.util.CLog;
import in.srain.cube.util.TimeUtils;
import in.srain.cube.util.internal.AppObserver;
import in.srain.cube.util.log.LogEvent;

import static com.xiaoyu.im.datamodel.messagesync.MessageFetchResult.COMPLETE;
import static com.xiaoyu.im.datamodel.messagesync.MessageFetchResult.HAS_MORE;

public class NimProxy extends AbstractProxy<IMMessage, NimConversationUnique, CustomNotification> {

    private static final String TAG = "NimProxy";
    private static final String VENDOR_TYPE = IMVendorType.NIM;
    private static final int MAX_LIMIT = 100;

    private static volatile NimProxy sInstance;

    private final ConcurrentHashMap<String, IMCallback<IMMessage>> mSendMessageCallbackMap = new ConcurrentHashMap<>();
    private final NimMessageBridge mMessageBridge;

    private NimProxy() {
        super(VENDOR_TYPE);
        mMessageBridge = new NimMessageBridge(this);
    }

    public static NimProxy getInstance() {
        if (sInstance == null) {
            synchronized (NimProxy.class) {
                if (sInstance == null) {
                    sInstance = new NimProxy();
                }
            }
        }
        return sInstance;
    }

    @Override
    public void setUp() {
        NIMClient.getService(MsgService.class).registerCustomAttachmentParser(new CustomAttachment.CustomAttachParser());

        final Observer<IMMessage> messageStatusObserver = (Observer<IMMessage>) message -> {
            CLog.d(IMConstant.LOG_TAG, "%s observeMsgStatus %s", TAG, NimMessageBridge.stringOfNimMessage(message));
            processMessageSendResult(message, null);
        };
        NIMClient.getService(MsgServiceObserve.class).observeMsgStatus(messageStatusObserver, true);

        LogEvent event = new LogEvent("login-sync-status");
        final Observer<LoginSyncStatus> loginSyncStatusObserver = (Observer<LoginSyncStatus>) status -> {
            if (status == LoginSyncStatus.BEGIN_SYNC) {
                event.startTimeForKey("rt");
                event.putData("status", "start");
                IMLogClient.Companion.getInstance().addLog(event);
            } else if (status == LoginSyncStatus.SYNC_COMPLETED) {
                event.endTimeForKey("rt");
                event.putData("status", "end");
                IMLogClient.Companion.getInstance().addLog(event);
                startRetryReceiveMessage();
            }
        };
        NIMClient.getService(AuthServiceObserver.class).observeLoginSyncDataStatus(loginSyncStatusObserver, true);
    }

    @Override
    public MessageBridge<IMMessage, NimConversationUnique, CustomNotification> getMessageBridge() {
        return mMessageBridge;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void vendorLogin(IMTokenInfo tokenInfo, IMCallback<Void> callback) {
        CLog.d(IMConstant.LOG_TAG, "%s vendorLogin", TAG);
        final RequestCallbackWrapper<Object> nimCallback = new RequestCallbackWrapper<Object>() {
            @Override
            public void onResult(int code, Object result, Throwable exception) {
                CLog.d(IMConstant.LOG_TAG, "%s vendorLogin %s %s %s", TAG, code, result, exception);
                final boolean success = code == ResponseCode.RES_SUCCESS;
                callback.onResult(success, null, tryCreateIMException(code, exception));
            }
        };
        final LoginInfo loginInfo = new LoginInfo(tokenInfo.vendorUid, tokenInfo.token, AppConfig.neteaseAppKey());
        NIMClient.getService(AuthService.class).login(loginInfo).setCallback(nimCallback);
    }

    @Override
    protected void vendorLogout() {
        CLog.i(TAG, "vendorLogout()");
        NIMClient.getService(AuthService.class).logout();
    }

    @Override
    protected void sendVendorMessage(NimConversationUnique unique, IMMessage message, IMCallback<IMMessage> callback) {
        mSendMessageCallbackMap.put(message.getUuid(), callback);
        final RequestCallbackWrapper<Void> nimCallback = new RequestCallbackWrapper<Void>() {
            @Override
            public void onResult(int code, Void result, Throwable exception) {
                CLog.d(IMConstant.LOG_TAG, "%s sendMessage %s %s %s", TAG, code, NimMessageBridge.stringOfNimMessage(message), exception);
                if (code != ResponseCode.RES_SUCCESS) {
                    processMessageSendResult(message, tryCreateIMException(code, exception));
                }
            }
        };
        NIMClient.getService(MsgService.class).sendMessage(message, false).setCallback(nimCallback);
    }

    private void processMessageSendResult(IMMessage message, @Nullable IMException exception) {
        CLog.d(IMConstant.LOG_TAG, "%s processMessageSendResult %s %s", TAG, exception, NimMessageBridge.stringOfNimMessage(message));
        final IMCallback<IMMessage> callback = mSendMessageCallbackMap.remove(message.getUuid());
        if (callback == null) {
            CLog.e(IMConstant.LOG_TAG, "%s processMessageSendResult not callback for message %s %s", TAG, exception, NimMessageBridge.stringOfNimMessage(message));
            return;
        }
        final boolean success = message.getStatus() == MsgStatusEnum.success;
        callback.onResult(success, message, exception);
    }

    @Override
    protected void recallVendorMessage(NimConversationUnique unique, IMMessage message, IMCallback<Void> callback) {
        final RequestCallbackWrapper<Void> nimCallback = new RequestCallbackWrapper<Void>() {
            @Override
            public void onResult(int code, Void result, Throwable exception) {
                CLog.d(IMConstant.LOG_TAG, "%s revokeMessage %s %s %s", TAG, code, NimMessageBridge.stringOfNimMessage(message), exception);
                boolean success = code == ResponseCode.RES_SUCCESS;
                callback.onResult(success, result, tryCreateIMException(code, exception));
            }
        };
        NIMClient.getService(MsgService.class).revokeMessage(message).setCallback(nimCallback);
    }

    @Override
    protected void observeReceiveVendorMessage(AppObserver<List<IMMessage>> receiveMessageObserver) {
        final Observer<List<IMMessage>> observer = (Observer<List<IMMessage>>) messages -> {
            CLog.d(IMConstant.LOG_TAG, "%s observeReceiveMessage %s", TAG, NimMessageBridge.stringOfListNimMessageForReceiveMessage(messages));
            receiveMessageObserver.onEvent(messages);
        };
        NIMClient.getService(MsgServiceObserve.class).observeReceiveMessage(observer, true);
    }

    @Override
    protected void observeReceiveVendorNotification(AppObserver<CustomNotification> observer) {
        final Observer<CustomNotification> vendorObserver = (Observer<CustomNotification>) notification -> {
            CLog.d(IMConstant.LOG_TAG, "%s observeCustomNotification %s", TAG, NimMessageBridge.stringOfNimCustomNotificationForReceiveNotification(notification));
            observer.onEvent(notification);
        };
        NIMClient.getService(MsgServiceObserve.class).observeCustomNotification(vendorObserver, true);
    }

    @Override
    public void observeBroadcastMessage(AppObserver<String> broadcastMessageObserver) {
        final Observer<BroadcastMessage> observer = (Observer<BroadcastMessage>) broadcastMessage -> {
            CLog.d(IMConstant.LOG_TAG, "%s observeBroadcastMessage %s %s %s %s", TAG,
                    broadcastMessage.getId(), broadcastMessage.getFromAccount(),
                    broadcastMessage.getTime(), broadcastMessage.getContent());
            broadcastMessageObserver.onEvent(broadcastMessage.getContent());
        };
        NIMClient.getService(MsgServiceObserve.class).observeBroadcastMessage(observer, true);
    }

    @Override
    protected void observeRecallVendorMessage(AppObserver<IMMessage> revokeMessageObserver) {
        final Observer<RevokeMsgNotification> observer = (Observer<RevokeMsgNotification>) notification -> {
            IMMessage message = notification.getMessage();
            CLog.d(IMConstant.LOG_TAG, "%s observeRevokeMessage %s", TAG, NimMessageBridge.stringOfNimMessage(message));
            revokeMessageObserver.onEvent(message);
        };
        NIMClient.getService(MsgServiceObserve.class).observeRevokeMessage(observer, true);
    }

    @Override
    public void observeOnlineStatus(AppObserver<String> onlineStatusObserver) {
        final Observer<StatusCode> observer = (Observer<StatusCode>) statusCode -> {
            CLog.d(IMConstant.LOG_TAG, "%s observeOnlineStatus %s", TAG, statusCode);
            onlineStatusObserver.onEvent(getOnlineStatus(statusCode));
        };
        NIMClient.getService(AuthServiceObserver.class).observeOnlineStatus(observer, true);
    }

    @Override
    public void observeOnlineUserAccounts(AppObserver<Set<String>> onlineUserAccountsObserver) {
        final Observer<List<Event>> observer = (Observer<List<Event>>) events -> {
            events = EventFilter.getInstance().filterOlderEvent(events);
            if (events == null) {
                CLog.i(TAG, "observeOnlineUserAccounts() events is null");
                return;
            }
            CLog.i(TAG, "observeOnlineUserAccounts() events size=%s", events.size());
            Set<String> onlineAccounts = filterOnlineStatusEvent(events);
            CLog.i(TAG, "observeOnlineUserAccounts() size=%s, list=%s", onlineAccounts.size(), TextUtils.join(",", onlineAccounts));
            onlineUserAccountsObserver.onEvent(onlineAccounts);
        };
        NIMClient.getService(EventSubscribeServiceObserver.class).observeEventChanged(observer, true);
    }

    private Set<String> filterOnlineStatusEvent(List<Event> events) {
        Set<String> onlineStateEvents = new HashSet<>();
        for (Event event : events) {
            boolean isOnlineStatusEvent = NimOnlineStateEvent.isOnlineStateEvent(event);
            boolean isOnline = event.getEventValue() == 1; // online value
            if (isOnlineStatusEvent && isOnline) {
                onlineStateEvents.add(event.getPublisherAccount());
            }
        }
        return onlineStateEvents;
    }

    @Override
    public void resubscribeUsersOnlineStatus(List<String> oldAccids, List<String> newAccids) {
        if (ListUtil.isNullOrEmpty(oldAccids)) {
            return;
        }

        unsubscribeUsersOnlineStatus(oldAccids, new RequestCallback<Void>() {
            @Override
            public void onSuccess(Void param) {
                subscribeUsersOnlineStatus(newAccids);
            }

            @Override
            public void onFailed(int code) {
            }

            @Override
            public void onException(Throwable exception) {
            }
        });
    }

    @Override
    public void subscribeUsersOnlineStatus(List<String> accids) {
        if (ListUtil.isNullOrEmpty(accids)) {
            return;
        }
        EventSubscribeRequest subscribeRequest = new EventSubscribeRequest();
        subscribeRequest.setPublishers(accids);
        subscribeRequest.setEventType(NimEventType.ONLINE_STATE.getValue());
        subscribeRequest.setExpiry(TimeUtils.SECONDS_IN_MINUTES);
        subscribeRequest.setSyncCurrentValue(true);
        CLog.i(TAG, "subscribeUsersOnlineStatus() size=%s, list=%s", accids.size(), TextUtils.join(",", accids));

        NIMClient.getService(EventSubscribeService.class).subscribeEvent(subscribeRequest).setCallback(new RequestCallbackWrapper<List<String>>() {
            @Override
            public void onResult(int code, List<String> result, Throwable exception) {
                CLog.i(TAG, "subscribeUserOnlineEvent() code=%s, list=%s", code, TextUtils.join(",", accids));
                if (code == ResponseCode.RES_SUCCESS) {
                    new OnlineUserAccountEvent(result).post();
                }
            }
        });
    }

    @Override
    public void unsubscribeUsersOnlineStatus(List<String> accids, RequestCallback<Void> callback) {
        EventSubscribeRequest unsubscribeRequest = new EventSubscribeRequest();
        unsubscribeRequest.setPublishers(accids);
        unsubscribeRequest.setEventType(NimEventType.ONLINE_STATE.getValue());
        CLog.i(TAG, "unsubscribeUsersOnlineStatus() size=%s, list=%s", accids.size(), TextUtils.join(",", accids));
        NIMClient.getService(EventSubscribeService.class).batchUnSubscribeEvent(unsubscribeRequest).setCallback(callback);
    }

    @Override
    public int getVendorUnreadMessageCount(NimConversationUnique unique) {
        final RecentContact recentContact = NIMClient.getService(MsgService.class).queryRecentContact(unique.vendorCid, unique.sessionType);
        if (recentContact == null) {
            CLog.e(IMConstant.LOG_TAG, "%s getVendorUnreadMessageCount cannot find recent contact for %s", TAG, unique.vendorCid);
            return 0;
        }
        CLog.d(IMConstant.LOG_TAG, "%s getVendorUnreadMessageCount recent contact %s %s", TAG, unique.vendorCid, recentContact);
        return recentContact.getUnreadCount();
    }

    @Override
    public void clearVendorUnreadCount(NimConversationUnique unique) {
        NIMClient.getService(MsgService.class).clearUnreadCount(unique.vendorCid, unique.sessionType);
    }

    @Override
    public void sendVendorMessageReceipt(IMMessage message) {
        NIMClient.getService(MsgService.class).sendMessageReceipt(message.getSessionId(), message);
    }

    @Override
    public void observeMessageReceipt(AppObserver<List<AppMessageReceipt>> messageReceiptObserver) {
        Observer<List<MessageReceipt>> observer = messageReceipts -> {
            CLog.d(IMConstant.LOG_TAG, "%s observeMessageReceipt %s", TAG, NimMessageBridge.stringOfListMessageReceipt(messageReceipts));
            AppThreads.runOnDBThread(() -> {
                List<AppMessageReceipt> receipts = ListUtil.convertToListFilterNull(messageReceipts, messageReceipt -> {
                    boolean conversationEmpty = ConversationManager.getInstance().isConversationEmpty();
                    // If observer had been called, but conversations didn't load from DB, load them at first.
                    if (conversationEmpty) {
                        CLog.d(IMConstant.LOG_TAG, "observeMessageReceipt conversation map is empty, start load from DB");
                        ConversationManager.getInstance().loadConversationListFromDB();
                    }

                    NimConversationUnique unique = NimConversationUnique.createOrNull(messageReceipt.getSessionId(), SessionTypeEnum.None);
                    if (unique == null) {
                        CLog.d(IMConstant.LOG_TAG, "%s observeMessageReceipt unique is null, vendorCid=%s, time=%s", TAG, messageReceipt.getSessionId(), messageReceipt.getTime());
                        return null;
                    }

                    String chatId = ChatIdMapper.getInstance().getChatIdByVendor(unique);
                    if (chatId == null) {
                        CLog.d(IMConstant.LOG_TAG, "%s observeMessageReceipt chatId is null, vendorCid=%s, time=%s", TAG, messageReceipt.getSessionId(), messageReceipt.getTime());
                        return null;
                    }
                    return new AppMessageReceipt(chatId, messageReceipt.getTime());
                });

                messageReceiptObserver.onEvent(receipts);
            });
        };

        NIMClient.getService(MsgServiceObserve.class).observeMessageReceipt(observer, true);
    }

    @Override
    protected void queryVendorMessagesFromRemote(int limit, NimConversationUnique unique, long fromTime, long toTime, @QueryDirection.DirectionDef int direction, IMCallback<List<IMMessage>> callback) {
        final IMMessage fromMessage = MessageBuilder.createEmptyMessage(unique.vendorCid, unique.sessionType, fromTime);
        queryVendorMessagesFromRemoteInternal(limit, fromMessage, toTime, direction, callback);
    }

    private void queryVendorMessagesFromRemoteInternal(int limit, @NonNull IMMessage lastNimMessage, long toTime, @QueryDirection.DirectionDef int direction, IMCallback<List<IMMessage>> callback) {
        final RequestCallbackWrapper<List<IMMessage>> nimCallback = new RequestCallbackWrapper<List<IMMessage>>() {
            @Override
            public void onResult(int code, List<IMMessage> result, Throwable exception) {
                result = ListUtil.emptyIfNull(result);
                CLog.d(IMConstant.LOG_TAG, "%s pullMessageHistoryEx %s %s %s %s %s %s %s", TAG, limit, lastNimMessage.getUuid(), toTime, false, code, NimMessageBridge.stringOfListNimMessage(result), exception);
                final boolean success = code == ResponseCode.RES_SUCCESS;
                callback.onResult(success, result, tryCreateIMException(code, exception));
            }
        };
        boolean isQueryNew = direction == QueryDirection.NEW;
        NIMClient.getService(MsgService.class).pullMessageHistoryEx(lastNimMessage, toTime, limit, isQueryNew ? QueryDirectionEnum.QUERY_NEW : QueryDirectionEnum.QUERY_OLD, true).setCallback(nimCallback);
    }

    public void queryNimMessagesByUUID(String uuid, IMCallback<IMMessage> callback) {
        final RequestCallbackWrapper<List<IMMessage>> vendorCallback = new RequestCallbackWrapper<List<IMMessage>>() {
            @Override
            public void onResult(int code, List<IMMessage> result, Throwable exception) {
                CLog.d(IMConstant.LOG_TAG, "%s queryMessageListByUuid %s %s %s %s", TAG, uuid, code, result, exception);
                final boolean success = !ListUtil.isNullOrEmpty(result);
                callback.onResult(success, ListUtil.getFirst(result), tryCreateIMException(code, exception));
            }
        };
        NIMClient.getService(MsgService.class).queryMessageListByUuid(Collections.singletonList(uuid)).setCallback(vendorCallback);
    }

    @Nullable
    private static IMException tryCreateIMException(int code, Throwable t) {
        return code == ResponseCode.RES_SUCCESS ? null : new IMException(t, code);
    }

    @IMOnlineStatus.IMOnlineStatusDef
    @Override
    public String getOnlineStatus() {
        final StatusCode statusCode = NIMClient.getStatus();
        return getOnlineStatus(statusCode);
    }

    @IMOnlineStatus.IMOnlineStatusDef
    private String getOnlineStatus(StatusCode statusCode) {
        switch (statusCode) {
            case UNLOGIN:
                return IMOnlineStatus.UNLOGIN;
            case NET_BROKEN:
                return IMOnlineStatus.DISCONNECTED;
            case CONNECTING:
                return IMOnlineStatus.CONNECTING;
            case LOGINING:
                return IMOnlineStatus.LOGINING;
            case SYNCING:
                return IMOnlineStatus.SYNCING;
            case LOGINED:
                return IMOnlineStatus.LOGINED;
            case KICKOUT:
                return IMOnlineStatus.KICKOUT;
            case KICK_BY_OTHER_CLIENT:
                return IMOnlineStatus.KICK_BY_OTHER_CLIENT;
            case FORBIDDEN:
                return IMOnlineStatus.FORBIDDEN;
            case VER_ERROR:
                return IMOnlineStatus.VER_ERROR;
            case PWD_ERROR:
                return IMOnlineStatus.PWD_ERROR;
            default:
                return IMOnlineStatus.INVALID;
        }
    }

    @Override
    public AntiSpamResult checkLocalAntiSpam(String srcText, String replacement) {
        final LocalAntiSpamResult nimResult = NIMClient.getService(MsgService.class).checkLocalAntiSpam(srcText, replacement);
        final int operator = nimResult.getOperator();
        final boolean replaced = operator == 1;
        final String destText = replaced ? nimResult.getContent() : srcText;
        CLog.d(IMConstant.LOG_TAG, "%s checkLocalAntiSpam srcText: %s replacement: %s operator: %s destText: %s", TAG, srcText, replacement, operator, destText);
        return new AntiSpamResult(operator, destText);
    }

    @NonNull
    @Override
    public MessageFetchResult onFetch(String chatId, long fromTime, long toTime, @QueryDirection.DirectionDef int direction) throws Throwable {
        final List<AppMessage> messages = queryMessagesFromRemote(MAX_LIMIT, chatId, fromTime, toTime, direction);

        if (ListUtil.isNullOrEmpty(messages)) {
            return new MessageFetchResult(COMPLETE, fromTime, messages);
        } else {
            int size = messages.size();
            long lastFromTime = messages.get(size - 1).time + 1;
            return new MessageFetchResult(HAS_MORE, lastFromTime, messages);
        }
    }
}
